本文转载自sysfs、udev 和 它们背后的 Linux 统一设备模型

详细内容请参考统一设备模型

1. sysfs 诞生之前

一切皆文件,这是 Linux 的哲学之一。设备当然也不例外,它们往往被抽象成文件,存放在 /dev 目录下供用户进程进行操作。用户通过这些设备文件,可以实现对硬件进行相应的操作。而这些设备文件,需要由对应的设备文件系统来负责管理。

在 kernel 2.6 之前,完成这一使命的是 devfs。devfs 是 Linux 2.4 引入的一个虚拟的文件系统,挂载在 /dev 目录下。可以动态地为设备在 /dev 下创建或删除相应的设备文件,只生成存在设备的节点。

然而它存在以下缺点:

  • 可分配的设备号数目 (major / minor) 受到限制
  • 设备映射不确定,一个设备所对应的设备文件可能发生改变
  • 设备名称在内核或模块中写死,违反了内核开发的原则
  • 缺乏热插拔机制

随着 kernel 的发展,从 Linux 2.6 起,devfs 被 sysfs + udev 所取代。sysfs + udev 在设计哲学和现实中的易用性都比 devfs 更优,自此 sysfs + udev 的组合走上 mainline ,直至目前,依然作为 Linux 的设备管理手段。

2. sysfs

sysfs 是一个基于内存的虚拟的文件系统,由 kernel 提供,挂载到 /sys 目录下(用 mount 查看得到 sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime)),负责以设备树的形式向 user space 提供直观的设备和驱动信息。

sysfs 以不同的视角展示当前系统接入的设备:

  • /sys/block 历史遗留问题,存放块设备,提供以设备名 (如 sda) 到 / sys/devices 的符号链接

  • /sys/bus(对应kernel中的 struct bus_type) 按总线类型分类,在某个总线目录之下可以找到连接该总线的设备的符号链接,指向 / sys/devices。某个总线目录之下的 drivers 目录包含了该总线所需的所有驱动的符号链接。

  • /sys/class(对应kernel中的struct class) 按设备功能分类,如输入设备在 /sys/class/input 之下,图形设备在 /sys/class/graphics 之下,是指向 /sys/devices 目录下对应设备的符号链接。

  • /sys/dev(对应kernel中的struct device_driver)按设备驱动程序分层(字符设备/块设备),提供以 major:minor 为名到/sys/devices 的符号链接。

  • /sys/devices(对应kernel中的struct device) 包含所有被发现的注册在各种总线上的各种物理设备。
    所有的物理设备都按其在总线上的拓扑结构来显示,除了 platform devices 和 system devices:

    • platform devices 一般是挂在芯片内部高速或者低速总线上的各种控制器和外设,能被 CPU 直接寻址。
    • system devices 不是外设,他是芯片内部的核心结构,比如 CPU,timer 等,他们一般没有相关的 driver,但是会有一些体系结构相关的代码来配置他们。
  • /sys/firmware 提供对固件的查询和操作接口(关于固件有专用于固件加载的一套API)。

  • /sys/fs 描述当前加载的文件系统,提供文件系统和文件系统已挂载设备信息。

  • /sys/hypervisor 如果开启了 Xen,这个目录下会提供相关属性文件。

  • /sys/kernel 提供 kernel 所有可调整参数,但大多数可调整参数依然存放在 sysctl(/proc/sys/kernel)。

  • /sys/module 所有加载模块 (包括内联、编译进 kernel、外部的模块) 的信息,按模块类型分类。

  • /sys/power 电源选项,可用于控制整个机器的电源状态,如写入控制命令进行关机、重启等。

sysfs 支持多视角查看,通过符号链接,同样的信息可以出现在多个目录下。

以硬盘 sda 为例,既可以在块设备目录/sys/block/下找到,又可以在所有设备目录/sys/devices/pci0000:00/0000:00:10.0/host32/target32:0:0/ 下找到。

查看 sda1 设备目录下的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$ ll /sys/block/sda/
drwxr-xr-x 11 root root 0 Feb 3 04:32 ./
drwxr-xr-x 3 root root 0 Feb 3 04:32 ../
-r--r--r-- 1 root root 4096 Feb 3 04:32 alignment_offset
lrwxrwxrwx 1 root root 0 Feb 3 04:32 bdi -> ../../../../../../../virtual/bdi/8:0/
-r--r--r-- 1 root root 4096 Feb 3 04:32 capability
-r--r--r-- 1 root root 4096 Feb 3 04:32 dev
lrwxrwxrwx 1 root root 0 Feb 3 04:32 device -> ../../../2:0:0:0/
-r--r--r-- 1 root root 4096 Feb 3 04:32 discard_alignment
-r--r--r-- 1 root root 4096 Feb 3 04:32 events
-r--r--r-- 1 root root 4096 Feb 3 04:32 events_async
-rw-r--r-- 1 root root 4096 Feb 3 04:32 events_poll_msecs
-r--r--r-- 1 root root 4096 Feb 3 04:32 ext_range
drwxr-xr-x 2 root root 0 Feb 3 04:32 holders/
-r--r--r-- 1 root root 4096 Feb 3 04:32 inflight
drwxr-xr-x 2 root root 0 Feb 3 04:32 integrity/
drwxr-xr-x 2 root root 0 Feb 3 04:32 power/
drwxr-xr-x 3 root root 0 Feb 3 04:32 queue/
-r--r--r-- 1 root root 4096 Feb 3 04:32 range
-r--r--r-- 1 root root 4096 Feb 3 04:32 removable
-r--r--r-- 1 root root 4096 Feb 3 04:32 ro
drwxr-xr-x 5 root root 0 Feb 3 04:32 sda1/
drwxr-xr-x 5 root root 0 Feb 3 04:32 sda2/
drwxr-xr-x 5 root root 0 Feb 3 04:32 sda5/
-r--r--r-- 1 root root 4096 Feb 3 04:32 size
drwxr-xr-x 2 root root 0 Feb 3 04:32 slaves/
-r--r--r-- 1 root root 4096 Feb 3 04:32 stat
lrwxrwxrwx 1 root root 0 Feb 3 04:32 subsystem -> ../../../../../../../../class/block/
drwxr-xr-x 2 root root 0 Feb 3 04:32 trace/
-rw-r--r-- 1 root root 4096 Feb 3 04:32 uevent

目录以文件的形式提供了设备的信息,比如 dev 记录了主设备号和次设备号,size 记录了分区大小,uevent 存放了 uevent 的标识符等:

1
2
$ cat /sys/block/sda/size
41943040

3. 统一设备模型

sysfs 的功能基于 Linux 的统一设备模型,其由以下结构构成:

3.1 Kobject

目前为止,Kobject主要提供如下功能:

  1. 通过parent指针,可以将所有Kobject以层次结构的形式组合起来。
  2. 使用一个引用计数(reference count),来记录Kobject被引用的次数,并在引用次数变为0时把它释放。
  3. 和sysfs虚拟文件系统配合,将每一个Kobject及其特性,以文件的形式,开放到用户空间。

在描述数据结构之前,有必要说明一下Kobject, Kset和Ktype这三个概念。

  • Kobject是基本数据类型,每个Kobject都会在”/sys/“文件系统中以目录的形式出现。

  • Ktype代表Kobject(严格地讲,是包含了Kobject的数据结构)的属性操作集合(由于通用性,多个Kobject可能共用同一个属性操作集,因此把Ktype独立出来了)。

  • Kset是一个特殊的Kobject(因此它也会在”/sys/“文件系统中以目录的形式出现),它用来集合相似的Kobject(这些Kobject可以是相同属性的,也可以不同属性的)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct kobject {
const char *name; // 名称,将在 sysfs 中作为目录名
struct list_head entry; // 加入 kset 链表的结构
struct kobject *parent; // 父节点指针,构成树状结构
struct kset *kset; // 指向所属 kset
struct kobj_type *ktype; // 类型
struct kernfs_node *sd; // 指向所属 (sysfs) 目录项
struct kref kref; // 引用计数
#ifdef CONFIG_DEBUG_KOBJECT_RELEASE
struct delayed_work release;
#endif
unsigned int state_initialized:1; // 是否已经初始化
unsigned int state_in_sysfs:1; // 是否已在 sysfs 中显示
unsigned int state_add_uevent_sent:1; // 是否已经向 user space 发送 ADD uevent
unsigned int state_remove_uevent_sent:1; // 是否已经向 user space 发送 REMOVE uevent
unsigned int uevent_suppress:1; // 是否忽略上报(不上报 uevent)
};
1
2
3
4
5
6
7
struct kobj_type {
void (*release)(struct kobject *kobj); // 析构函数,kobject 的引用计数为 0 时调用
const struct sysfs_ops *sysfs_ops; // 操作函数,当用户读取 sysfs 属性时调用 show(),写入 sysfs 属性时调用 store()
struct attribute **default_attrs; // 默认属性,体现为该 kobject 目录下的文件
const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj); // namespace 操作函数
const void *(*namespace)(struct kobject *kobj);
};
1
2
3
4
5
6
struct kset {
struct list_head list; // kobject 链表头
spinlock_t list_lock; // 自旋锁,保障操作安全
struct kobject kobj; // 自身的 kobject
const struct kset_uevent_ops *uevent_ops; // uevent 操作函数集。kobject 发送 uevent 时会调用所属 kset 的 uevent_ops
};

总结:

  • Kobject的核心功能是:保持一个引用计数,当该计数减为0时,自动释放Kobject所占用的meomry空间。这就决定了Kobject必须是动态分配的(只有这样才能动态释放)。

  • Kobject大多数的使用场景,是内嵌在大型的数据结构中,因此这些大型的数据结构,也必须是动态分配、动态释放的。那么释放的时机是什么呢?是内嵌的Kobject释放时。但是Kobject的释放是由Kobject模块自动完成的(在引用计数为0时),那么怎么一并释放包含自己的大型数据结构呢?

  • 这时Ktype就派上用场了。我们知道,Ktype中的release回调函数负责释放Kobject(甚至是包含Kobject的数据结构)的内存空间,那么Ktype及其内部函数,是由谁实现呢?是由上层数据结构所在的模块!因为只有它,才清楚Kobject嵌在哪个数据结构中,并通过Kobject指针以及自身的数据结构类型,找到需要释放的上层数据结构的指针,然后释放它。

  • 每一个内嵌Kobject的数据结构,例如device、device_driver等等,都要实现一个Ktype,并定义其中的回调函数。同理,sysfs相关的操作也一样,必须经过ktype的中转,因为sysfs看到的是Kobject,而真正的文件操作的主体,是内嵌Kobject的上层数据结构。

3.2 device / driver / bus / class

详细内容参考Linux设备模型(1)_基本概念

device / driver / bus / class 四者之间存在着这样的关系:

  • driver 用于驱动 device ,其保存了所有能够被它所驱动的设备链表。
  • bus 是连接 CPU 和 device 的桥梁,其保存了所有挂载在它上面的设备链表和驱动这些设备的驱动链表。
  • class 用于描述一类 device ,其保存了所有该类 device 的设备链表。

3.3 attribute

用于定义设备模型中的各项属性。基本属性有两种,分别为普通属性 attribute 和二进制属性 bin_attribute

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct attribute {
const char *name; // 属性名
umode_t mode;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
bool ignore_lockdep:1;
struct lock_class_key *key;
struct lock_class_key skey;
#endif
};

struct bin_attribute {
struct attribute attr;
size_t size;
void *private;
ssize_t (*read)(struct file *, struct kobject *, struct bin_attribute *,
char *, loff_t, size_t);
ssize_t (*write)(struct file *, struct kobject *, struct bin_attribute *,
char *, loff_t, size_t);
int (*mmap)(struct file *, struct kobject *, struct bin_attribute *attr,
struct vm_area_struct *vma);
};

使用 attribute 生成的 sysfs 文件,只能用字符串的形式读写。而 struct bin_attribute 在 attribute 的基础上,增加了 read、write 等函数,因此它所生成的 sysfs 文件可以用任何方式读写。

4. udev && uevent

udev: Device Manager for the Linux Kernel in Userspace很好地介绍了udev。

4.1 What is udev?

udev (userspace /dev) is a device manager for the Linux kernel. As the successor of devfsd and hotplug, udev primaily manages device nodes in the /dev directory. At the same time, udev also handls all user space events raised when hardware devices are added into the system or removed from it, including firmware loading as reuqired by certain devices.

4.2 Why was udev developed?

/dev directory is where all device files for the system are loaded (note that everything in Linux is files, so are devices). This directory had been managed by devfs filesystem until Linux kernel version 2.5. The introduction of devfs solved some problem, however, still many problems remained.

udev was started to solve all of those problems, and its goals are:

  • Run in userspace (doing so we save kernel memory space that was wasted by saving device naming rules)
  • Create a dynamic /dev (automatically creates or removes device entries in /dev when devices are inserted or removed)
  • Provide consistent device naming
  • Provide a userspace API to access info about current system devices

4.3 How to use udev?

看完使用udev修改设备默认名称可以理解udev的使用。

4.4 udev的构成

  1. libudev 函数库,提供获取设备信息的接口
  2. udevd 处于 user namespace 的管理软件。管理 / dev 下的设备文件。
  3. udevadm 命令行工具。可用来向 udevd 发送指令。

4.5 uevent的功能

uevent是Kobject的一部分,用于在Kobject状态发生改变时,例如增加、移除等,通知用户空间程序。用户空间程序收到这样的事件后,会做相应的处理。

该机制通常是用来支持热拔插设备的,例如U盘插入后,USB相关的驱动软件会动态创建用于表示该U盘的device结构(相应的也包括其中的kobject),并告知用户空间程序,为该U盘动态的创建/dev/目录下的设备节点,更进一步,可以通知其它的应用程序,将该U盘设备mount到系统中,从而动态的支持该设备。

4.6 uevent在kernel中的位置

下面图片描述了uevent模块在内核中的位置:

由此可知,uevent的机制是比较简单的,设备模型中任何设备有事件需要上报时,会触发uevent提供的接口。uevent模块准备好上报事件的格式后,可以通过两个途径把事件上报到用户空间:一种是通过kmod模块,直接调用用户空间的可执行文件;另一种是通过netlink通信机制,将事件从内核空间传递给用户空间。

4.7 mdev

  • udev是基于netlink机制的,它在系统启动时运行了一个deamon程序udevd,通过监听内核发送的uevent 来执行相应的热拔插动作,包括创建/删除设备节点,加载/卸载驱动模块等等。
  • mdev是基于uevent_helper机制的,它在系统启动时修改了内核中的uevnet_helper 变量(通过写/proc/sys/kernel/hotplug),值为“/sbin/mdev”。
  • udev 使用的netlink 机制在有大量uevent 的场合效率高,适合用在PC 机上;而mdev 使用的uevent_helper 机制实现简单,适合用在嵌入式系统中。

参考资料:

  1. Linux Udev
  2. 嵌入式Linux——uevent机制:uevent原理分析
  3. udev: Device Manager for the Linux Kernel in Userspace
  4. 使用udev修改设备默认名称
  5. mdev和udev机制并不相同